feat(pam): add Oracle DB access support#192
Conversation
Adds Oracle DB as the 8th PAM handler. Gateway accepts client connections with a placeholder password, proxies pre-auth bytes verbatim to upstream, intercepts at O5Logon to swap client-supplied placeholder-keyed material for real-password-keyed material, and byte-relays post-auth. Credential injection works end-to-end for JDBC thin clients (sqlcl, SQL Developer, DBeaver) and go-ora; user never sees real Oracle credentials. Handler lives in packages/pam/handlers/oracle/. Ports crypto primitives (PBKDF2+SHA512, AES-CBC session-key encryption, PKCS5 padding) and TTC codec (compressed ints, CLR byte strings, KVP encoding) from MIT-licensed sijms/go-ora. See ATTRIBUTION.md and ORACLE_PAM_NOTES.md for architecture notes and handoff. Known dead code remains in this commit from the earlier full- impersonation attempt (ano.go, nego.go, nego_templates.go, parts of o5logon*.go, upstream.go, handshake_test.go). Kept intact to preserve history of the approach; cleanup follows as a separate commit.
The initial attempt used server-side Oracle impersonation (see prior
commit). That design worked through authentication but hit a
state-mismatch problem post-auth: upstream (via go-ora) and client
negotiated different TTC capabilities, so relayed queries were
rejected as protocol violations.
The replacement — proxied-auth — is already in proxy_auth.go and is
the flow wired to HandleConnection. This commit removes the dead
files and vestigial symbols that supported the old path:
Removed entirely:
- ano.go, nego.go, nego_templates.go (pre-auth TNS/TTC negotiation
handlers; pre-auth is now forwarded verbatim to upstream)
- upstream.go (go-ora-based upstream dial + KVP extraction;
replaced by dialUpstreamRaw in proxy_auth.go)
- handshake_test.go (tested the impersonation path, orphaned)
Pruned:
- proxy.go: handleConnectionLegacy (~200 LOC)
- o5logon.go: O5LogonServerState, NewO5LogonServerState,
VerifyClientPassword, deriveKey11g, md5Hash, parseIntVal
- o5logon_server.go: AuthPhaseOne, ParseAuthPhaseOne,
BuildAuthPhaseOneResponse, BuildAuthPhaseTwoResponse,
BuildAuthPhaseTwoResponseFromUpstream, RunServerO5Logon,
dumpBytes, readUint32
- tns.go: AcceptPacket, AcceptFromConnect, ConnectPacket,
ParseConnectPacket, MarkerPacketBytes (we forward raw packet
bytes rather than parse/build CONNECT or ACCEPT)
Kept: crypto primitives, DATA packet codec, TTC reader/builder,
query logger, prependedConn, error helpers — all live in the
proxied-auth flow.
Drops github.com/sijms/go-ora/v2 from go.mod — no longer imported.
Adds .idea and .vscode to .gitignore. Updates ORACLE_PAM_NOTES.md
with a current-state header; historical sections below retained
for context.
Net: ~1,600 LOC removed. The handler directory goes from 12 files
to 8. Build, fmt, and the Oracle SQL test matrix (SELECT, INSERT,
DDL, PL/SQL, bind vars, NLS queries) still pass against sqlcl →
gateway → AWS RDS Oracle 19c.
The TTC query extractor was logging empty strings for OALL8 payloads because tryExtractSQL's "skip 6 compressed ints then read a CLR" heuristic didn't land on the SQL text — the OALL8 wire format has variable-length headers that differ by client driver and bind pattern. As a result, session recordings contained only session headers (124 bytes) with no actual query content. Replace structured parsing with a simple scan for the longest printable ASCII run in the payload. In practice the SQL text is always the longest such run. Verified with sqlcl: .enc file grows from 124 bytes (empty) to ~880 bytes (with captured queries) for a SELECT + COMMIT session. This only affects content — the tap, packet demultiplexing, and encrypted file I/O were all working correctly. Fix is localised to tryExtractSQL.
sqlcl (and other JDBC thin clients) frequently bundle a piggybacked OCLOSE for the previous cursor with the next OALL8 query in a single TTC packet. The previous parser checked byte 0 for 0x03 (function call) and bailed when it saw 0x11 (piggyback marker), missing the OALL8 underneath. Scan the payload for the function-call + OALL8 byte pair instead, so the parser finds the query regardless of any preceding piggyback prefix. Same treatment for COMMIT and ROLLBACK, which also get piggybacked.
Matches the other SQL handlers' pattern: the client speaks plain TCP to our local listener, we do TLS to the upstream database and translate in the middle. No change to the client-facing UX — the same JDBC URL that works against a plain-TCP Oracle resource now also works against a TCPS-enabled one. Implementing this correctly required discovering Oracle TCPS's two-handshake flow (by reading go-ora's network/session.go readPacket RESEND branch — credit there): 1. Dial TCP, do an initial TLS handshake. Forward the client's CONNECT through this first TLS session. 2. When the upstream RESEND packet's byte-5 flag has 0x08 set, Oracle expects the client to abandon the first TLS session and run a FRESH TLS handshake on the bare TCP socket. Server drops its first-round TLS state in lockstep. We mirror this via upgradeToTLS on the same rawConn, then continue the Oracle handshake through the new session. 3. Mask byte 5 on packets going downstream so thin clients (JDBC thin, python-oracledb thin) don't see the 0x08 signal and try to cast their local TcpNTAdapter to TcpsNTAdapter — the cast would fail because the client-to-proxy socket is plain TCP. 4. Accept TLS 1.0 as the floor for the upstream dial: Oracle 19c's second-round handshake negotiates down to 1.0 in some configurations (AWS RDS's SSL option being one of them). The outer ALPN mTLS tunnel remains TLS 1.2+. Also removes now-dead SSLRejectUnauthorized / SSLCertificate fields from OracleProxyConfig — the shared TLSConfig built in pam-proxy.go carries that information already. Verified end-to-end against AWS RDS Oracle 19c (SSL option, port 2484) with sqlcl: authentication, DDL, DML+COMMIT, PL/SQL, DBMS_OUTPUT, bind variables, and session recording all work. Plain-TCP Oracle path is unchanged.
Pure comment cleanup on buildOracleTLSConfig — no behavior change.
- ATTRIBUTION.md: drop reference to nego.go (deleted in the impersonation-era cleanup) and add the upstream TCPS two-handshake flow adaptation from go-ora's session.readPacket RESEND branch. - o5logon_server.go: the file header still described the impersonation-era architecture where the gateway acted as an Oracle server and drove the O5Logon exchange. Rewrite it to describe the current proxied-auth role — packet-layer helpers used by the byte- level O5Logon translation in proxy_auth.go — and drop the reference to upstream.go, which was removed in 3ff9cff.
Two warnings, both from the original feat commit (4b25ec2): - Dead self-assignment state.ServerSessKey = state.ServerSessKey on the phase-2 request translation path. The inline comment "no-op; kept for clarity" already admitted it was pointless. Delete. - Unreachable _ = prefix after a return statement inside replaceKvpValueKeepingSize. The prefix variable was left over from a refactor — the new code uses oldStart / oldEnd instead — and the _ = prefix trick to silence "declared but not used" landed on the wrong side of the return. Delete both the dead prefix declaration and the unreachable suppression. Also drops valStart (only used by the dead prefix computation). No behavior change; go vet is now clean on the oracle handler.
Two UX simplifications to the Oracle PAM access command: 1. Stop creating a per-session TNS_ADMIN directory and printing an `export TNS_ADMIN=...` line in the connect instructions. The directory only existed to hold a sqlnet.ora that set DISABLE_OOB=TRUE — a defence against sqlcl's out-of-band Ctrl-C signalling that we never actually observed breaking. If a real interrupt problem surfaces we can revisit with a proper fix instead of a per-session file dance. 2. Change ProxyPasswordPlaceholder from "infisical-pam-proxy" to "password". The string value is cryptographically arbitrary — Oracle's O5Logon needs the client and gateway to agree on SOME string; any works. Shorter is easier to copy-paste. The accompanying "not a real credential" note in the CLI output stays.
Three items that snuck in but aren't part of Oracle PAM: 1. Untrack ORACLE_PAM_NOTES.md. It's a development notes/research log that belongs with scratch work, not in the branch. The file stays on disk (uncommitted) for local reference. Its content is also stale since the placeholder password changed from "infisical-pam-proxy" to "password" in 3ee7951 without matching updates to the notes. 2. Revert the github.com/emirpasic/gods indirect bump from v1.12.0 to v1.18.1 in go.mod/go.sum. This was residue from when go-ora/v2 was temporarily added as a direct dep in the initial feat commit — go-ora needed the newer gods, and Go module MVS held the bumped version even after go-ora was removed in 3ff9cff. Running `go mod tidy` against current HEAD (with main's go.mod/go.sum as the baseline) produces no further changes, confirming nothing we ship actually needs v1.18.1. .gitignore additions (.vscode, .idea) stay — reasonable hygiene that doesn't hurt the PR and will prevent future editor-file noise.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3bfa003ff8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…lers - Password plaintext no longer embedded in "password mismatch" error (was bubbling to gateway logs via zerolog's .Err chain). - Long Oracle passwords (≥ 96 chars) now encode correctly: replaceKVPValue routes AUTH_PASSWORD through TTCBuilder.PutClr so values above the short-form threshold emit the 0xFE chunked form instead of a truncated single-byte length. - Client-supplied username rewritten to InjectUsername in phase-1 and phase-2 auth requests. Matches the effect of how the postgres/mysql/mssql handlers overwrite the startup-packet user — the client's choice becomes inert; upstream always looks up the configured account's verifier. - Dead / misleading code removed: local PacketTypeResendMarker constant that duplicated tns.go's PacketTypeResend, package-level min() shadowing the Go 1.21+ builtin, the if !use32Bit branch in extractDataPayload where both arms assigned the same value, and the now-unused encodeCompressedInt helper.
Static-analysis sweep (staticcheck + manual cross-file grep) across the oracle handler package. All removals are symbols that were defined but never referenced anywhere: - tns.go: markerTypeReset, markerTypeInterrupt constants. - o5logon_server.go: TTCMsgAuthResponse, TTCMsgBreak, LogonModeUserAndPass, LogonModeNoNewPass constants; AuthPhaseTwo fields ESpeedyKey / AlterSession / ClientInfo / LogonMode (parser wrote them, no reader downstream); ParseAuthPhaseTwo's KVP switch trimmed to the two keys actually consumed (AUTH_SESSKEY, AUTH_PASSWORD). - ttc.go: TTCReader.GetNullTermString(), TTCReader.SetUseBigClrChunks() — both uncalled. Also added a proper attribution header to o5logon_server.go (it was adapting go-ora's phase-2 layout + summary-object format but lacked the same kind of header the other ported files have), and expanded ATTRIBUTION.md to cover it plus the specific primitives borrowed by o5logon.go. No behavior change. go build / go vet / staticcheck clean on the package.
Exhaustive dead-export sweep via `go doc -all` (a more reliable check than my earlier regex-based grep, which missed untyped constants). Removed: - VerifierType10g / VerifierType11g / VerifierType12c — defined but no callers; the only verifier type our code implements (18453, 12c+ PBKDF2+SHA512) is hardcoded, the three named constants were never referenced. The 18453-specific comments in the code retain the documentation. - ORA12660EncryptionRequired — defined but no callers. Only ORA1017InvalidCredentials is actually emitted. Post-sweep: every exported symbol in the oracle handler package has a call site. `go build` / `go vet` / `staticcheck` clean.
staticcheck's U1000 has an exemption for const blocks: if any member is used, sibling members aren't flagged as unused even when they are. An exhaustive sweep that enumerates every top-level identifier via manual grep (so const-block membership is irrelevant) caught the ones staticcheck skipped. Removed: - tns.go: PacketTypeAbort, PacketTypeAck, PacketTypeAttn, PacketTypeCtrl, PacketTypeNull. Only the 7 PacketType values we actually dispatch on remain. - query_logger.go: ttcFuncOFETCH, ttcFuncOCLOSE, ttcFuncOSTMT, ttcFuncOLOGOFF, ttcMsgPiggyback. Only the 4 TTC opcodes the query tap actually looks for remain (OALL8, OCOMMIT, ORLLBK, the outer msgFunction). Post-sweep: exhaustive enumeration finds zero unused symbols across the package (189 candidates checked). go build / go vet / staticcheck clean.
…e banner The banner was printing the real upstream DB username (pamResponse.Metadata["username"]) in the connection URL, even though the preceding "Account:" label already shows the Infisical account name. Since the gateway now rewrites the client-supplied username in the O5Logon exchange to the configured real user, the client can (and should) connect using the account name — and the banner makes that explicit. Before: Resource: aws-oracledb Account: admin2 oracle://admin:password@localhost:53521/DATABASE ← confusing After: Resource: aws-oracledb Account: admin2 oracle://admin2:password@localhost:53521/DATABASE ← matches the label Scope: Oracle only for now. The postgres/mysql/mssql handlers also overwrite the client username on the wire, so the same banner change would work there, but that needs a separate verification pass per dialect before we extend it.
The client's CONNECT description string was forwarded unchanged, requiring users to know the real Oracle service name. Now we rewrite SERVICE_NAME to match InjectDatabase from the vault config, consistent with how username and password are already injected.
The gateway no longer checks whether the client sent the placeholder password "password". It unconditionally encrypts the real password from the vault, regardless of what the client typed. Auth will succeed either way since we inject the real credentials. The placeholder password is still shown to the user in the CLI banner and still used for key derivation in phase-1 — only the verification check is removed.
…SE regen AUTH_SVR_RESPONSE is encrypted with encKey, which is derived from session keys + CSK salt — not the password. The client and Oracle derive the same encKey, so Oracle's original proof is already valid for the client. No need to regenerate it. Removes translatePhase2Response, BuildSvrResponse, and the placeholderEncKey field from ProxyAuthState.
…t drain The post-ACCEPT supplement peek used a 3-second read deadline to detect whether a go-ora client sent connect-data as a separate packet. This added a fixed 3s delay for every non-go-ora client (sqlcl, JDBC thin). Instead, check the CONNECT packet structure: if connect-data-length + connect-data-offset exceeds the packet size, the data wasn't inline and a supplement will follow. Track whether the RESEND handler already consumed it; if not, do a blocking read (the supplement is guaranteed to be in the TCP buffer since the client sent it before waiting for a response). Removes prependedConn, detectConnectDataSupplement, and the timeout.
…formatting - Remove verbose comments across all files (~20% → ~2% comment rate) - Remove per-file go-ora attribution (ATTRIBUTION.md carries the license) - Trim ATTRIBUTION.md to just the copyright notice + MIT text - Make InjectDatabase mandatory (error if empty, always overwrite client's SERVICE_NAME) - Format unknown Oracle error codes as ORA-XXXXX instead of bare "ERROR" - Clarify ProxyPasswordPlaceholder as a decoy
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 32113716 | Triggered | Generic Password | 2051eea | dev/pam/resources/mssql/entrypoint.sh | View secret |
| 32113717 | Triggered | Generic Password | 2051eea | dev/pam/resources/mongodb/init/seed.js | View secret |
| 32113718 | Triggered | Username Password | 2051eea | dev/pam/docker-compose.yml | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
|
💬 Discussion in Slack: #pr-review-cli-192-feat-pam-add-oracle-db-access-support Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel. |
Description 📣
Adds Oracle as a PAM database resource
Infisical/infisical#6134
Type ✨
Tests 🛠️
# Here's some code block to paste some code snippets